基础语法

Rust 是强类型语言,但是其 let 关键字可以充当 C++ auto 的角色。另外,其类型声明是后置类型声明:

let a:i32 = 10;

Rust 中的变量分为三种:

  • 常量:使用 const 声明的变量,不允许更改值

  • 不可变变量:使用 let 声明的变量,只能通过多次声明更改变量值

  • 可变变量:使用 let mut 声明的变量,可以随意更改值

常量必须要指定类型,不可变变量在多次声明的过程中允许改变类型。子作用域中的声明不会更改父作用域变量的值

不允许窄化转换
非零值不能转换为布尔值
没有自增和自减运算符

数据类型

Rust 中内置了诸如 i32, u32 这些宏,另外还添加了 isize, usize 这两个大小与 CPU 字宽度相同的类型

浮点数分为 f32, f64 两种,默认会使用 f64,因为两者处理速度差不多,但是 f64 精度更高

Rust 中的字符大小为 4 字节,只允许 UTF-8 编码

元组使用圆括号声明,数组使用中括号声明:

let a = (510, 1.2, 'c');
let b = [10, 20, 30];
println!("{}", a.2);
println!("{}", b[0]);

f32 和 f64 只实现了 PartialEq 和 PartialOrd,这是因为 float 类型中存在两个特殊的值:

  • f64:NAN:Not a Number。

  • f64:INFINITY 和 f64:NEG_INFINITY:无穷大

前者不是一个数值,无法进行数值运算。后者是一个无穷值,亦无法进行比较。

注释

注释方式和 C++ 一样

函数

函数的声明方式也是后置类型声明:

fn main() {
   println!("{}", add(1, 2));
}

fn add(a: i32, b: i32) -> i32 {
   return a + b;
}

函数不需要前置类型声明,只要你代码中能找到这个函数就行,写前面还是写后面都行

另外,对于返回值有一种简写形式:

fn add(a: i32, b: i32) -> i32 {
   a + b
}

需要注意的是,后面不能跟分号

匿名函数

Rust 中最基础的定义匿名函数的方式如下:

let s = String::from("你好");

let lam = |s_str: &str| {
    println!("{}", s_str);
};

这种方式和普通的函数没什么不同。

闭包

和 C++ 相同,如果 Rust 捕获了外部的变量,则成为一个闭包。但是 Rust 会自动计算捕获列表,无需手动指明:

let s = String::from("你好");

let lam = || {
    println!("{}", s);
};

如上述代码所示,Lambda 表达式中可以使用前面的变量。默认的捕获方式为 不可变引用

下面是闭包捕获变量的方式:

捕获方式示例

不可变引用

let lambda = ||{};

可变引用

let mut s = String::from("你好");

let mut lam =|| {
    s.push('a');
    println!("{}", s);
};

移动捕获

let mut s = String::from("你好");

let mut lam = move || {
    s.push('a');
    println!("{}", s);
};
Rust 中可变引用是独占的。一旦你在 Lambda 表达式中捕获了可变引用,那么在最后一次 Lambda 之前,就不能再修改被捕获的变量了。

闭包的捕获模式

闭包在捕获变量时的顺序如下: [1]

  1. 不可变引用

  2. 唯一不可变引用

  3. 可变引用

  4. 移动

结构体、元组、枚举等复合类型始终作为一个整体被捕获,无法捕获单个字段。

变量被捕获的方式与变量在闭包中的使用方式有关,而与上下文无关(例如变量的生命周期等)。

如果使用 move 关键字,则变量只能通过拷贝(实现 Copy trait)或者移动被捕获。

条件语句

条件语句中的条件无需加小括号:

fn main() {
   let number = 3;
   if number < 2 {
      println!("number < 2");
   } else if number < 5 {
      println!("number < 5");
   } else {
      println!("number >= 5");
   }
}

另外还能实现类似 C++ 三目运算符的效果:

let number = if number > 2 { number*2} else {0};

循环

let mut n = 1;
while n != 4 {
   println!("{}", n);
   n += 1;
}
let a = [10, 20, 30];
for i in a {
   println!("{}", i);
}
for i in 0..5 {
   println!("{}", i);
}
let _m = loop {
   if n == 10 {
      break n;
   }
   n += 1;
};

Rust 中没有 for 循环,只有 while 和 for_each 循环。另外 loop 循环相当于 while(true) 。但是不同的是可以使用 break 返回一个值

loop 在单独成块时末尾无需加分号,但是作为初始化语句的一部分时需要加分号(毕竟是语句)

这里之所以将变量命名为 _m 是因为我的 Rust 将 Warning 视为 Error,这里有命名 Warning

所有权

Rust 也有作用域规则,并将表达式分为值和变量两部分,遵循以下规则:

  • 栈中的值默认执行拷贝语义

  • 堆中的值默认执行移动语义

  • 变量可以引用其它变量

例如:

#![cfg_attr(
   debug_assertions,
   allow(dead_code, unused_imports, unused_variables, unused_mut)
)]

fn main() {
   let s_a = 10;
   let s_b = s_a; // 执行拷贝语义
   let h_a = String::from("hello");
   let h_b = h_a; // 执行移动语义,此处 h_a 会失效
   let mut h_c = h_b.clone(); // 执行拷贝
   print_s(h_b); // 执行移动语义,此处 h_b 会失效
   let r_a = &h_c; // r_a 是指向 h_c 的只读引用
   let r_b = &mut h_c; // r_b 是指向 h_c 的可写引用
}

fn print_s(str: String) {
   println!("{}", str);
}

第一行代码是为了关闭 unused warning,不关掉的话代码没法通过编译

Rust 将引用的过程称为租赁

另外,Rust 没有 free 或者 delete,它会在作用域结束时自动为你添加资源清理代码

与 C++ 不同的是,引用不会影响原宿主的生命周期,当原宿主失效时,引用必须重新绑定:

#![cfg_attr(
   debug_assertions,
   allow(dead_code, unused_imports, unused_variables, unused_mut)
)]

fn main() {
   let s1 = String::from("hello");
   let r = &s1;
   let s2 = s1; // 移交 s1 所有权, r 必须重新绑定
   let r = &s2;
}

悬垂引用在编译期就会被发现。

切片

Rust 和 Golang 一样也具备切片。切片是对一片内存区域的引用。

  • 不可变切片类似于 const*const 指针。

  • 可变切片类似于 *const 指针。

fn main() {
   let s1 = "hello"; // s1 是 &str 类型
   let mut s2 = String::from("hello"); // s2 是 String 类型
   let slice1 = &s2[0..3]; // slice1 是 &str 类型
   let slice2 = &s2[3..];
   let slice3 = &s2[..3];
   println!("{}", slice1);
   println!("{}", slice2);
   println!("{}", slice3);
}

结构体

使用结构体

struct Student {
   id: u32,
   name: String,
}

fn main() {
   let stu = Student {
      id: 1234,
      name: String::from("小明"),
   };
   let stu2 = Student {
      id: 2345,
      ..stu // stu 中非 id 字段被移动
   };
   // println!("{}", stu.name); // 此处不允许,stu.name 已经被移动
   println!("{}", stu2.name);
}

Rust 中的结构体无需加分号结尾,stu2 展示了另一种语法:当新的结构体 至少 有一个字段不同时,可以简化语法,这种语法我认为可以被成为更新

另外还有一个类元组的结构体:

struct Color(u32, u32, u32);

fn main() {
   let black = Color(0, 0, 0);
}

这种结构体的使用方式与元组相同,但是元组结构体是一个语句,末尾需要添加分号

方法

Rust 可以将方法绑定到结构体上,具体语法为:

impl Struct {
    // 需要绑定到 Struct 上的函数
}
  • 当 func 具备 self 参数时,类似 C++ 中的成员函数。

  • 当 func 不具备 self 参数时,类似 C++ 中的静态成员函数

struct Int {
   data: u32,
}

impl Int {
   fn create(data: u32) -> Int {
      return Int { data };
   }
   fn bigger(&self, b: &Int) -> bool {
      return self.data > b.data;
   }
}

fn main() {
   let a = Int::create(10);
   let b = Int::create(11);
   println!("{}", a.bigger(&b));
}

impl 可以写任意次,总的效果相当于他们的并集

构造函数

Rust 存在两种构造函数:

  • 基于约定的 new 方法。

  • Default trait。

Rust 一般约定使用 new 来构造一个对象。此外还能通过 Default trait 支持默认构造函数:


/// Time in seconds.
///
/// # Example
///
/// ```
/// let s = Second::default();
/// assert_eq!(0, s.value());
/// ```
pub struct Second {
    value: u64
}

impl Second {
    /// Returns the value in seconds.
    pub fn value(&self) -> u64 {
        self.value
    }
}

impl Default for Second {
    fn default() -> Self {
        Self { value: 0 }
    }
}

如果所有类型的字段都实现了 Default,还能派生 Default:

/// Time in seconds.
///
/// # Example
///
/// ```
/// let s = Second::default();
/// assert_eq!(0, s.value());
/// ```
#[derive(Default)]
pub struct Second {
    value: u64
}

impl Second {
    /// Returns the value in seconds.
    pub fn value(&self) -> u64 {
        self.value
    }
}
  • 当实现了 Default 时,不建议提供一个没有参数的 new 方法。

  • Default 可以被用于 or_default 函数。

枚举

定义枚举

枚举的三种写法:

enum Book {
   Papery,
   Electronic,
}

enum Anmial {
   Dog(String),
   Cat(String),
}

enum Pair {
   Number { number: u32 },
   String { str: String },
}

模式匹配

枚举中可以储存值,这些值要取出来就需要使用 match 控制流。以标准库中的 Option 为例:

Option 定义
enum Option<T> {
    None,
    Some(T),
}

使用方式为:

let opt = Some(10);

match opt {
    Some(v) => {
        println!("{}", v)
    }
    Option::None => {}
}

match 还提供了 _ 充当 default 语义:

match opt {
    Some(v) => {
        println!("{}", v)
    }
    _ => {}
}

match 还可以提供更复杂的表示:

let a: Result<i32, ()> = Ok(10);
match a {
    Ok(v) if v > 10 => {
        println!(">10");
    }
    /// match v 同时会将值绑定到 o 上。
    o @ Ok(v) if v > 5 => {
        o.unwrap();
    }
    Ok(v) => {
        println!("v < 5, v is {v}")
    }
    Err(_err) => {
        unreachable!()
    }
}

if let

if let 语句用于匹配枚举中的一种情形,失配情形则直接忽略:

let opt = Some(10);

if let Some(v) = opt {
    println!("{}", v)
}

集合

vector

vector 和 C++ 中 vector 的底层数据相同。使用方式类似:

let mut v: Vec<i32> = Vec::new();
v.push(10);
v.push(20);

for i in &v { // 使用引用以防止 v 丢失容器的所有权
    println!("{}", i);
}

创建容器的另一种方式是使用宏:

let v = vec![1, 2, 3];

vec 访问元素有两种方式:

访问方式返回值错误方式

下标访问

对应的值

触发 panic

使用 get

Option

let v = vec![1, 2, 3];

let res = v.get(10);
if let None = res {
    println!("索引越界");
}
当持有 vec 的引用时,无法向容器中添加更多的值。其根本原因是因为当 vec 容量不够时会重新开辟内存空间,并将以前的数据复制过去,从而导致引用失效。

map

和其它语言中的 map 类似。map 要求 key 类型一致,value 类型一致。由于 HashMap 类型不在 rust 的 prelude 中,因此需要手动引入:

use std::collections::HashMap;

fn main() {
    let mut map: HashMap<String, i32> = HashMap::new();
    map.insert(String::from("a"), 10);
    map.insert(String::from("b"), 20);

    for (k, v) in &map {
        println!("{}: {}", k, v);
    }
}

HashMap 的访问方式和 vector 相同,也有两种方式,且行为相同。

覆盖新值

map 的 insert 方法默认会覆盖掉旧值,这时可以使用 entry 方法,当且仅当没有 key 时才插入:

let mut map: HashMap<String, i32> = HashMap::new();
map.entry(String::from("a")).or_insert(50);

字符串

Rust 字符串有两种形式:

  • 最基础的字符串是 &str 类型。这类似于 C++ 中的 char*

  • String 是对 str 的封装

两种形式都原生支持宽字节类型,因而无法使用下标的方式进行引用。

当使用 String 的切片生成 str 类型时,如果切片落在了宽字符中间,就会导致程序 panic

String 的生成方式一般有两种:

生成方式要求
String::from

参数实现了 Display trait

fromat!

fromat 的一般使用方式如下:

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = fromat!("{s1}-{s2}-{s3}");

泛型

简单的泛型语法:

struct Number<T> {
   data: T,
}

impl<T> Number<T> {
   fn get_data(&self) -> &T {
      return &self.data;
   }
}

fn main() {
   let a = Number { data: 10 };
   let b = Number { data: 11 };
   println!("{}", a.get_data());
}

和 C++ 一样,函数也允许有泛型,这是 impl 中也需要添加相应的类型,而且泛型的具体类型可以被推断出来。推断时不会发生类型转换

错误处理

Rust 将错误分为可恢复的错误和不可恢复的错误。

不可恢复的错误类似 C++ 中断言失败的宏:

fn main() {
   panic!("failed");
}

可恢复错误通过 Result<T,E> 枚举 表示,可能产生异常的函数的返回值都是 Result 类型的:

fn main() {
   let f = File::open("hello.txt");
   match f{
      Ok(file) =>{
            println!("file opened");
      },
      Err(err) =>{
            println!("Failed");
      }
   }
}

实际上报错时显示类型是 tuple

使用 if let 语法可以简化处理流程:

fn main() {
   let f = File::open("hello.txt");
   if let Ok(file) = f {
      println!("File opened successfully.");
   } else {
      println!("Failed to open the file.");
   }
}

直接对 Result 调用 unwrap/expect 会导致系统直接挂掉

因为异常只是一个类型,所以传递时直接返回就行了,异常还有一个简单语法:

fn g(i: i32) -> Result<i32, bool> {
   let t = f(i)?;
   Ok(t) // 因为确定 t 不是 Err, t 在这里已经是 i32 类型
}

这里函数 f 的返回值是 Result,其 E 的类型必须和 g 的 E 的类型相同,? 运算符用来将异常取出,如果有异常直接返回,否则向下继续执行

生命周期参数

来看一个返回引用的函数的例子:

fn longer(s1: &str, s2: &str) -> &str {
   if s1.len() > s1.len() {
      return s1;
   }
   return s2;
}

fn main() {
   let s1 = "he";
   let s2 = "she";
   let r = longer(&s1, &s2);
   print!("{}", r);
}

遗憾的是 Rust 不会允许编译的,根本原因在于 longer 返回的是一个引用,但是引用的值的生命周期不知道。可以通过生命周期参数解决这个问题

下面我会将术语生命周期进行加粗,而和 C++ 中生命周期相同的那个术语不会

生命周期参数的语法是一个单引号后跟一个小写字母,习惯上以 'a 表示。生命周期注解描述了多个引用生命周期相互的关系,而不影响其生命周期。

现在使用生命周期参数来纠正这个函数:

fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
   if s1.len() > s1.len() {
      return s1;
   }
   return s2;
}

这个函数的签名分为了三个部分:

  • fn longer<'a> 中使用泛型的语法将生命周期参数引入函数

  • s1: &'a str 表明 s1 的生命周期至少与 'a 的生命周期一样长

  • &'a str 表示返回值的生命周期至少与 'a 一样长

'a 的生命周期是 s1 和 s2 中生命周期的较小值

下面无法通过编译的代码,从侧面证实了生命周期参数不会延长值的生命周期:

fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
   if s1.len() > s1.len() {
      return s1;
   }
   return s2;
}

fn main() {
   let r;
   {
      let s1 = String::from("he");
      let s2 = String::from("she");
      r = longer(s1.as_str(), s2.as_str());
   }
   print!("{}", r);
}

生命周期注释有一个特别的:'static 。所有用双引号包括的字符串常量所代表的精确数据类型都是 &'static str ,'static 所表示的生命周期从程序运行开始到程序运行结束,因此下面的代码可以通过编译:

let r;
{
   let s1 = "he";
   let s2 = "she";
   r = longer(s1, s2);
}
print!("{}", r);

生命周期参数从语法上可以简单地认为就是一个特殊的模板参数

模块、包和箱

简单地来讲,含有 Cargo.toml 文件的项目就是一个包,项目编译后生成的二进制文件就是箱

Rust 中的模块类似于 C++ 中命名空间和 Python 中模块的并集,遵循以下原则:

  1. 如果没有显式表明模块,则每个文件就代表一个模块(类似 Python)

  2. 如果显式声明模块,则遵循声明(类似命名空间)

模块允许嵌套。只有平级或者更深层次的模块才允许访问私有的函数或者结构体。如果希望外部访问,必须使用 pub 公开

模块的使用方式与命名空间类似,同样是使用 :: ,但是导入运算符使用了 use 而不是 using

终端 IO

例如:

use std::io::stdin;
use std::io::BufRead;

fn main() {
   let args = std::env::args();
   for arg in args {
      println!("{}", arg);
   }
   // echo
   let mut buf = String::new();
   stdin().read_line(&mut buf).unwrap();
   println!("{}", buf);
}

文件 IO

先看一个单向读写的例子:

use std::fs;

fn main() {
   fs::write("1.txt", "hello").unwrap();
   let content = fs::read_to_string("1.txt").unwrap();
   println!("{:?}", content);
}

再看一个双向读写的例子:

use std::{
   fs,
   io::{Read, Seek, Write},
};

fn main() {
   let mut file = std::fs::OpenOptions::new()
      .write(true)
      .read(true)
      .open("1.txt")
      .unwrap();
   file.write(b"hello,world\n").unwrap();
   // 刷新缓冲区
   file.flush().unwrap();
   file.seek(std::io::SeekFrom::Start(0)).unwrap();
   // 读取数据
   let mut buf = String::new();
   file.read_to_string(&mut buf).unwrap();
   println!("{}", buf);
}

智能指针

智能指针是实现了 DerefDrop traits 的结构体。这两个 trait 分别具备以下功能:

Deref

可以被解引用

Drop

退出作用域时需要执行

Box

Box 是功能最少的一个智能指针。其除了数据被分配在堆上,和普通变量并没有什么区别。类似于 C++ 中的 ScopedGuard:

let b = Box::new("你好");
println!("{}", *b);

Rc

Rc 是具备引用计数的不可变智能指针,类似于 C++ 中的共享指针:

use std::rc::Rc;

fn main() {
    let b = Rc::new("你好");
    println!("{}", *b);
}

同样的,因为 rc 并不是 prelude 的一分子,因此需要实现声明。

Rc 的拷贝会引起引用计数的增加。

类型转换

类型转换一般有以下几种:

  • 隐式类型转换。

  • 使用 as 进行强制类型转换。

  • 使用 into 和 from 进行类型转换。

其中, as 作为隐式类型转换的补充出现,主要是为了填补隐式类型转换不允窄化类型转换的空缺。

into 和 from 作为两个 trait 来用允许自定义类型转换。此外 into 和 from 还有两个安全版本的 try_into 和 try_from。当失败时,返回 Error。

清除 Warning

如果需要关闭当前文件的 Warning,在当前文件顶部添加:

#[allow(dead_code)]

如果关闭 crate 的 Warning,需要在 main.rs 中添加:

#![allow(dead_code)]

格式化

以 println! 为例,格式化的方法有三种:

  • println!("{:?}", some_var);
  • println!{"{1:?}, {0}", var0, var1};
  • println!{"{var0:?}, {var1}"};

Cell,RefCell 和 UnsafeCell

UnsafeCell 提供了对内置对象的可变引用。UnsafeCell 使用 #[repr(transparent)] 来表示数据类型,因此 UnsafeCell 与包含的类型类型布局相同。

Cell 对使用了 Copy 类型的对象使用 Copy in 和 Copyout 实现可变引用。对实现了 Default 类型的对象使用 mem::replace 实现可变引用。由于 memmove 本事不是线程安全的,因此 Cell 也不是线程安全的。

RefCell 在 Cell 的基础上添加了对引用计数的检测。如果多次 borrow 会导致线程 panic。

Last moify: 2025-05-09 09:29:27
Build time:2025-07-18 09:41:42
Powered By asphinx